using System;
using System.Collections.Generic;
using System.Data;
using System.Linq;
using System.Web;
using System.Xml;

using CMS.Base;
using CMS.CMSImportExport;
using CMS.Core;
using CMS.DataEngine;
using CMS.EventLog;
using CMS.FormEngine;
using CMS.Globalization;
using CMS.Helpers;
using CMS.IO;
using CMS.MacroEngine;
using CMS.Membership;
using CMS.Modules;
using CMS.PortalEngine;
using CMS.Synchronization;
using CMS.URLRewritingEngine;
using CMS.WorkflowEngine;


#region "Code to bind to the ApplicationEvents using CMSModuleLoader"

/// <summary>
/// Upgrade loader
/// </summary>
[UpgradeLoader]
public partial class CMSModuleLoader
{
    /// <summary>
    /// Module registration
    /// </summary>
    private class UpgradeLoader : CMSLoaderAttribute
    {
        /// <summary>
        /// Initializes the module
        /// </summary>
        public override void PreInit()
        {
            ApplicationEvents.UpdateData.Execute += Update;
        }


        /// <summary>
        /// Updates the application data to a newer version if necessary
        /// </summary>
        private void Update(object sender, EventArgs eventArgs)
        {
            UpgradeProcedure.Update();
        }
    }
}

#endregion

/// <summary>
/// Class carrying the code to perform the upgrade procedure.
/// </summary>
public static class UpgradeProcedure
{
    #region "Variables"

    private static readonly string FORM_DEFINITION_TABLE = "Temp_FormDefinition";
    private static readonly string FORM_DEFINITION_NAME_COLUMN = "ObjectName";
    private static readonly string FORM_DEFINITION_VALUE_COLUMN = "FormDefinition";

    // Path to the upgrade package
    private static string mUpgradePackagePath;
    private static string mWebsitePath;
    private static string mEventLogSource;

    #endregion


    #region "Properties"

    /// <summary>
    /// Gets the source text for event log records generated by upgrade actions
    /// </summary>
    private static string EventLogSource
    {
        get
        {
            return mEventLogSource ?? (mEventLogSource = string.Format("Upgrade to {0}", CMSVersion.MainVersion));
        }
    }

    #endregion


    #region "Main update method"

    /// <summary>
    /// Runs the update procedure.
    /// </summary>
    public static void Update()
    {
        if (DatabaseHelper.IsDatabaseAvailable)
        {
            try
            {
                string version = SettingsKeyInfoProvider.GetValue("CMSDataVersion");
                switch (version.ToLowerCSafe())
                {
                    case "8.1":
                        using (var context = new CMSActionContext())
                        {
                            context.LogLicenseWarnings = false;

                            UpgradeApplication(Upgrade81To82, "8.2", "Upgrade_81_82.zip");
                        }
                        break;
                }
            }
            catch (Exception ex)
            {
                EventLogProvider.LogException(EventLogSource, "UPGRADE", ex);
            }
        }
    }

    #endregion


    #region "General purpose - all versions methods"

    private static void UpgradeApplication(Func<bool> versionSpecificMethod, string newVersion, string packageName)
    {
        // Increase the timeout for upgrade request due to expensive operations like macro signing and conversion (needed for large DBs)
        HttpContext.Current.Server.ScriptTimeout = 14400;

        EventLogProvider.LogInformation(EventLogSource, "START");

        // Set the path to the upgrade package (this has to be done here, not in the Import method, because it's an async procedure without HttpContext)
        mUpgradePackagePath = HttpContext.Current.Server.MapPath("~/CMSSiteUtils/Import/" + packageName);
        mWebsitePath = HttpContext.Current.Server.MapPath("~/");

        var dtm = new TableManager(null);
        using (var context = new CMSActionContext())
        {
            context.DisableLogging();
            context.CreateVersion = false;
            context.LogIntegration = false;

            if (dtm.TableExists(FORM_DEFINITION_TABLE))
            {
                UpdateClasses();
                UpdateAlternativeForms();
                DropTempDefinitionTable(dtm);
            }
        }

        // Update all views
        dtm.RefreshDocumentViews();
        RefreshCustomViews(dtm);

        // Set data version
        SettingsKeyInfoProvider.SetValue("CMSDataVersion", newVersion);
        SettingsKeyInfoProvider.SetValue("CMSDBVersion", newVersion);

        // Clear hashtables
        ModuleManager.ClearHashtables();

        // Clear the cache
        CacheHelper.ClearCache(null, true);

        // Drop the routes
        CMSDocumentRouteHelper.DropAllRoutes();

        // Init the Mimetype helper (required for the Import)
        MimeTypeHelper.LoadMimeTypes();

        // Call version specific operations
        if (versionSpecificMethod != null)
        {
            using (var context = new CMSActionContext())
            {
                context.DisableLogging();
                context.CreateVersion = false;
                context.LogIntegration = false;

                versionSpecificMethod.Invoke();
            }
        }

        // Import upgrade package with webparts, widgets...
        UpgradeImportPackage();

        RefreshMacroSignatures();

        EventLogProvider.LogInformation(EventLogSource, "FINISH");
    }


    /// <summary>
    /// Refreshes all custom views.
    /// </summary>
    private static void RefreshCustomViews(TableManager tm)
    {
        tm.RefreshView("View_CMS_User");
        tm.RefreshView("View_Community_Member");

        tm.RefreshView("View_COM_SKU");

        tm.RefreshView("View_NewsletterSubscriberUserRole_Joined");

        tm.RefreshView("View_Community_Group");

        tm.RefreshView("View_Community_Friend_Friends");
        tm.RefreshView("View_Community_Friend_RequestedFriends");

        tm.RefreshView("View_OM_Contact_Activity");
        tm.RefreshView("View_OM_Contact_Joined");
        tm.RefreshView("View_OM_ContactGroupMember_ContactJoined");

        tm.RefreshView("View_OM_Account_Joined");
        tm.RefreshView("View_OM_Account_MembershipJoined");
        tm.RefreshView("View_OM_ContactGroupMember_AccountJoined");
    }


    /// <summary>
    /// Update form definitions of classes (especially system tables).
    /// </summary>
    private static void UpdateClasses()
    {
        DataSet classes = GetFormDefinitions();
        if (!DataHelper.DataSourceIsEmpty(classes))
        {
            foreach (DataRow row in classes.Tables[0].Rows)
            {
                string objectName = DataHelper.GetStringValue(row, FORM_DEFINITION_NAME_COLUMN);
                string newDefinition = DataHelper.GetStringValue(row, FORM_DEFINITION_VALUE_COLUMN);

                if (!string.IsNullOrEmpty(objectName) && !string.IsNullOrEmpty(newDefinition))
                {
                    var dataClass = DataClassInfoProvider.GetDataClassInfo(objectName);
                    if (dataClass != null)
                    {
                        var newVersionFi = new FormInfo(newDefinition);

                        // Copy custom fields only for system tables
                        if (dataClass.ClassShowAsSystemTable)
                        {
                            var oldVersionFi = new FormInfo(dataClass.ClassFormDefinition);
                            CopyCustomFields(oldVersionFi, newVersionFi, false);
                        }

                        // Save the modified form definition
                        dataClass.ClassFormDefinition = newVersionFi.GetXmlDefinition();

                        // Update the scheme
                        dataClass.ClassXmlSchema = new TableManager(dataClass.ClassConnectionString).GetXmlSchema(dataClass.ClassTableName);

                        // Save the new definition
                        dataClass.Update();
                    }
                }
            }
        }
    }


    /// <summary>
    /// Updates an existing alternative forms form definitions. Appends existing custom fields to new version definitions.
    /// </summary>
    private static void UpdateAlternativeForms()
    {
        DataSet classes = GetFormDefinitions(true);
        if (!DataHelper.DataSourceIsEmpty(classes))
        {
            foreach (DataRow row in classes.Tables[0].Rows)
            {
                string objectName = DataHelper.GetStringValue(row, FORM_DEFINITION_NAME_COLUMN);
                string newDefinition = DataHelper.GetStringValue(row, FORM_DEFINITION_VALUE_COLUMN);

                if (!string.IsNullOrEmpty(objectName) && !string.IsNullOrEmpty(newDefinition))
                {
                    var altForm = AlternativeFormInfoProvider.GetAlternativeFormInfo(objectName);
                    if (altForm != null)
                    {
                        var mainDci = DataClassInfoProvider.GetDataClassInfo(altForm.FormClassID);
                        var classFormDefinition = mainDci.ClassFormDefinition;

                        if (altForm.FormCoupledClassID > 0)
                        {
                            // If coupled class is defined combine form definitions
                            var coupledDci = DataClassInfoProvider.GetDataClassInfo(altForm.FormCoupledClassID);
                            if (coupledDci != null)
                            {
                                classFormDefinition = FormHelper.MergeFormDefinitions(classFormDefinition, coupledDci.ClassFormDefinition);
                            }
                        }

                        var oldVersionDefinition = FormHelper.MergeFormDefinitions(classFormDefinition, altForm.FormDefinition);
                        var newVersionDefinition = FormHelper.MergeFormDefinitions(classFormDefinition, newDefinition);

                        var newVersionFi = new FormInfo(newVersionDefinition);
                        var oldVersionFi = new FormInfo(oldVersionDefinition);

                        CopyCustomFields(oldVersionFi, newVersionFi, true);

                        // Save the modified form definition
                        altForm.FormDefinition = FormHelper.GetFormDefinitionDifference(classFormDefinition, newVersionFi.GetXmlDefinition(), true);
                        altForm.Update();
                    }
                }
            }
        }
    }


    /// <summary>
    /// Copies custom fields from old version of form definition to the new form definition.
    /// </summary>
    /// <param name="oldVersionFi">Old version form definition</param>
    /// <param name="newVersionFi">New version form definition</param>
    /// <param name="overwrite">Indicates whether existing fields should be overwritten. Alternative form fields need to be overwritten due to the combination with upgraded class form.</param>
    private static void CopyCustomFields(FormInfo oldVersionFi, FormInfo newVersionFi, bool overwrite)
    {
        // Remove all system fields from old definition to get only custom fields
        oldVersionFi.RemoveFields(f => f.System);

        // Combine forms so that custom fields from old definition are appended to the new definition
        newVersionFi.CombineWithForm(oldVersionFi, new CombineWithFormSettings
        {
            IncludeCategories = false,
            RemoveEmptyCategories = true,
            OverwriteExisting = overwrite
        });
    }


    /// <summary>
    /// Returns dataset with class names (or alt.form full names) and form definitions which should be used for the upgrade.
    /// </summary>
    /// <param name="getAltForms">Indicates if alt.form definitions should be returned</param>
    private static DataSet GetFormDefinitions(bool getAltForms = false)
    {
        string queryText = String.Format("SELECT [{0}], [{1}] FROM [{2}] WHERE {3}", FORM_DEFINITION_NAME_COLUMN, FORM_DEFINITION_VALUE_COLUMN, FORM_DEFINITION_TABLE, getAltForms ? "IsAltForm = 1" : "IsAltForm = 0 OR IsAltForm IS NULL");

        DataSet ds = null;
        try
        {
            ds = ConnectionHelper.ExecuteQuery(queryText, null, QueryTypeEnum.SQLQuery);
        }
        catch (Exception ex)
        {
            EventLogProvider.LogException(EventLogSource, "GETFORMDEFINITION", ex);
        }

        return ds;
    }


    /// <summary>
    /// Drops temporary table with classes' and alt.forms' form definitions.
    /// </summary>
    /// <param name="dtm">Table manager</param>
    private static void DropTempDefinitionTable(TableManager dtm)
    {
        try
        {
            dtm.DropTable(FORM_DEFINITION_TABLE);
        }
        catch (Exception ex)
        {
            EventLogProvider.LogException(EventLogSource, "DROPTEMPTABLE", ex);
        }
    }


    /// <summary>
    /// Procedures which automatically imports the upgrade export package with all WebParts, Widgets, Reports and TimeZones.
    /// </summary>
    private static void UpgradeImportPackage()
    {
        // Import
        try
        {
            RequestStockHelper.Remove("CurrentDomain", true);

            var importSettings = new SiteImportSettings(MembershipContext.AuthenticatedUser)
            {
                DefaultProcessObjectType = ProcessObjectEnum.All,
                SourceFilePath = mUpgradePackagePath,
                WebsitePath = mWebsitePath
            };

            using (var context = new CMSActionContext())
            {
                context.DisableLogging();
                context.CreateVersion = false;
                context.LogIntegration = false;

                ImportProvider.ImportObjectsData(importSettings);

                // Regenerate time zones
                TimeZoneInfoProvider.GenerateTimeZoneRules();

                // Delete the files for separable modules which are not install and therefore not needed
                DeleteWebPartsOfUninstalledModules();

                ImportMetaFiles(Path.Combine(mWebsitePath, "App_Data\\CMSTemp\\Upgrade"));
            }
        }
        catch (Exception ex)
        {
            EventLogProvider.LogException(EventLogSource, "IMPORT", ex);
        }
    }


    /// <summary>
    /// Refreshes macro signatures in all object which can contain macros.
    /// </summary>
    private static void RefreshMacroSignatures()
    {
        // Get object types
        var objectTypes = new List<string> {
                TransformationInfo.OBJECT_TYPE,
                UIElementInfo.OBJECT_TYPE,
                FormUserControlInfo.OBJECT_TYPE,
                SettingsKeyInfo.OBJECT_TYPE,
                AlternativeFormInfo.OBJECT_TYPE,
                DataClassInfo.OBJECT_TYPE,
                DataClassInfo.OBJECT_TYPE_SYSTEMTABLE,
                DataClassInfo.OBJECT_TYPE_CUSTOMTABLE,
                DataClassInfo.OBJECT_TYPE_DOCUMENTTYPE,
                PageTemplateInfo.OBJECT_TYPE,
                LayoutInfo.OBJECT_TYPE,
                CssStylesheetInfo.OBJECT_TYPE,
                WorkflowActionInfo.OBJECT_TYPE,
            };

        foreach (string type in objectTypes)
        {
            try
            {
                using (var context = new CMSActionContext())
                {
                    context.DisableLogging();
                    context.CreateVersion = false;
                    context.LogIntegration = false;

                    var infos = new InfoObjectCollection(type);
                    foreach (var info in infos)
                    {
                        MacroSecurityProcessor.RefreshSecurityParameters(info, "administrator", true);
                    }
                }
            }
            catch (Exception ex)
            {
                EventLogProvider.LogException(EventLogSource, "REFRESHMACROSIGNATURES", ex, 0, "Type: " + type);
            }
        }
    }


    /// <summary>
    /// Deletes the files for separable modules which are not install and therefore not needed.
    /// </summary>
    private static void DeleteWebPartsOfUninstalledModules()
    {
        var webPartsPath = mWebsitePath + "CMSWebParts\\";
        var files = new List<string>();

        var separableModules = new List<string>
        {
            ModuleName.BIZFORM, 
            ModuleName.BLOGS,
            ModuleName.COMMUNITY,
            ModuleName.ECOMMERCE,
            ModuleName.EVENTMANAGER,
            ModuleName.FORUMS,
            ModuleName.MEDIALIBRARY,
            ModuleName.MESSAGEBOARD,
            ModuleName.MESSAGING,
            ModuleName.NEWSLETTER,
            ModuleName.NOTIFICATIONS,
            ModuleName.ONLINEMARKETING,
            ModuleName.POLLS,
            ModuleName.PROJECTMANAGEMENT,
            ModuleName.REPORTING,
            ModuleName.STRANDSRECOMMENDER,
            ModuleName.CHAT,
        };

        foreach (var separableModule in separableModules)
        {
            // Add files from this folder to the list of files to delete if the module is not installed
            if (!ModuleEntryManager.IsModuleLoaded(separableModule))
            {
                var folderName = GetWebPartFolderName(separableModule);
                files.AddRange(GetAllFiles(webPartsPath + folderName));
            }
        }

        // Remove web parts for separated modules
        foreach (String file in files)
        {
            try
            {
                File.Delete(file);
            }
            catch (Exception ex)
            {
                EventLogProvider.LogException(EventLogSource, "DELETEWEBPARTS", ex, 0, "File: " + file);
            }
        }
    }


    /// <summary>
    /// Returns list of all files in given folder (recursively, from all subdirectories as well).
    /// </summary>
    /// <param name="folder">Folder to search in</param>
    private static List<String> GetAllFiles(String folder)
    {
        var files = new List<string>();

        files.AddRange(Directory.GetFiles(folder));

        var dirs = Directory.GetDirectories(folder);

        foreach (string dir in dirs)
        {
            files.AddRange(GetAllFiles(dir));
        }

        return files;
    }


    /// <summary>
    /// For given module returns it's folder name within CMSWebParts folder.
    /// </summary>
    /// <param name="moduleName">Name of the module</param>
    /// <returns></returns>
    private static string GetWebPartFolderName(string moduleName)
    {
        // Handle exceptions
        switch (moduleName)
        {
            case ModuleName.BIZFORM:
                return "BizForms";

            case ModuleName.BLOGS:
                return "Blogs";

            case ModuleName.NEWSLETTER:
                return "Newsletters";
        }

        // By default, trim "CMS." prefix from module name which will give us folder name withing CMSWebParts directory
        return moduleName.Substring(4);
    }


    /// <summary>
    /// Imports default metafiles which were changed in the new version.
    /// </summary>
    /// <param name="upgradeFolder">Folder where the generated metafiles.xml file is</param>
    private static void ImportMetaFiles(string upgradeFolder)
    {
        try
        {
            // To get the file use Phobos - Generate files button, Metafile settings.
            // Choose only those object types which had metafiles in previous version and these metafiles changed to the new version.
            String xmlPath = Path.Combine(upgradeFolder, "metafiles.xml");
            if (File.Exists(xmlPath))
            {
                XmlDocument xDoc = new XmlDocument();
                xDoc.Load(xmlPath);

                XmlNode metaFilesNode = xDoc.SelectSingleNode("MetaFiles");
                if (metaFilesNode != null)
                {
                    String filesDirectory = Path.Combine(upgradeFolder, "Metafiles");

                    using (new CMSActionContext { LogEvents = false })
                    {
                        foreach (XmlNode metaFile in metaFilesNode)
                        {
                            // Load metafiles information from XML
                            String objType = metaFile.Attributes["ObjectType"].Value;
                            String groupName = metaFile.Attributes["GroupName"].Value;
                            String codeName = metaFile.Attributes["CodeName"].Value;
                            String fileName = metaFile.Attributes["FileName"].Value;
                            String extension = metaFile.Attributes["Extension"].Value;
                            String fileGUID = metaFile.Attributes["FileGUID"].Value;
                            String title = (metaFile.Attributes["Title"] != null) ? metaFile.Attributes["Title"].Value : null;
                            String description = (metaFile.Attributes["Description"] != null) ? metaFile.Attributes["Description"].Value : null;

                            // Try to find correspondent info object
                            BaseInfo infoObject = BaseAbstractInfoProvider.GetInfoByName(objType, codeName);
                            if (infoObject != null)
                            {
                                int infoObjectId = infoObject.Generalized.ObjectID;

                                // Check if metafile exists
                                InfoDataSet<MetaFileInfo> metaFilesSet = MetaFileInfoProvider.GetMetaFilesWithoutBinary(infoObjectId, objType, groupName, "MetaFileGUID = '" + fileGUID + "'", null);
                                if (DataHelper.DataSourceIsEmpty(metaFilesSet))
                                {
                                    // Create new metafile if does not exists
                                    String mfFileName = String.Format("{0}.{1}", fileGUID, extension.TrimStart('.'));
                                    MetaFileInfo mfInfo = new MetaFileInfo(Path.Combine(filesDirectory, mfFileName), infoObjectId, objType, groupName);
                                    mfInfo.MetaFileGUID = ValidationHelper.GetGuid(fileGUID, Guid.NewGuid());

                                    // Set correct properties
                                    mfInfo.MetaFileName = fileName;
                                    if (title != null)
                                    {
                                        mfInfo.MetaFileTitle = title;
                                    }
                                    if (description != null)
                                    {
                                        mfInfo.MetaFileDescription = description;
                                    }

                                    // Save new meta file
                                    MetaFileInfoProvider.SetMetaFileInfo(mfInfo);
                                }
                            }
                        }

                        // Remove existing files after successful finish
                        String[] files = Directory.GetFiles(upgradeFolder);
                        foreach (String file in files)
                        {
                            File.Delete(file);
                        }
                    }
                }
            }
        }
        catch (Exception ex)
        {
            EventLogProvider.LogException(EventLogSource, "IMPORTMETAFILES", ex);
        }
    }

    #endregion


    #region "Update from 8.1 to 8.2"

    /// <summary>
    /// Handles all the specific operations for upgrade from 8.1 to 8.2.
    /// </summary>
    /// <returns></returns>
    private static bool Upgrade81To82()
    {
        RemoveAnalyticsExtender();
        RemoveGlobalShippingOptionsFromRecycleBin();
        EncryptStrandsCatalogFeedPasswords();

        return true;
    }

    #endregion


    #region "Private methods"

    /// <summary>
    /// Encrypts all Strands catalog feed passwords
    /// </summary>
    private static void EncryptStrandsCatalogFeedPasswords()
    {
        const string keyName = "CMSStrandsCatalogFeedPassword";

        // Get all not empty keys
        var keys = SettingsKeyInfoProvider.GetSettingsKeys()
                                            .Columns("SiteID, KeyValue")
                                            .WhereEquals("KeyName", keyName)
                                            .WhereNotEmpty("KeyValue");

        foreach (var key in keys)
        {
            // Encrypt password
            SettingsKeyInfoProvider.SetValue(keyName, key.SiteID, EncryptionHelper.EncryptData(key.KeyValue));
        }
    }


    /// <summary>
    /// Removes obsolete analytics extender from UI elements
    /// </summary>
    private static void RemoveAnalyticsExtender()
    {
        var analyticsUiElement = UIElementInfoProvider.GetUIElementInfo("CMS.Content", "Analytics");
        if (analyticsUiElement != null)
        {
            RemoveAnalyticsExtenderFromUiElement(analyticsUiElement);
        }

        var orderRulesUiElement = UIElementInfoProvider.GetUIElementInfo("CMS.Ecommerce", "OrderRules");
        if (orderRulesUiElement != null)
        {
            RemoveAnalyticsExtenderFromUiElement(orderRulesUiElement);
        }

        AssignExtenderToShippingOptionTabs();
    }

    /// <summary>
    /// Removes Analytics Extender from given UIElement
    /// </summary>
    private static void RemoveAnalyticsExtenderFromUiElement(UIElementInfo uiElement)
    {
        XmlData customData = new XmlData();
        customData.LoadData(uiElement.ElementProperties);

        string pageExtenderClassName = ValidationHelper.GetString(customData.GetValue("pageextenderclassname"), String.Empty);

        if ("analyticsextender".EqualsCSafe(pageExtenderClassName.ToLower()))
        {
            customData.Remove("pageextenderclassname");
            customData.Remove("pageextenderassemblyname");
            uiElement.ElementProperties = customData.GetData();
            uiElement.Update();
        }
    }


    /// <summary>
    /// Assigns ShippingOptionTabsExtender to Edit.Configuration.ShippingOptionProperties UI element
    /// </summary>
    private static void AssignExtenderToShippingOptionTabs()
    {
        var element = UIElementInfoProvider.GetUIElementInfo(ModuleName.ECOMMERCE, "Edit.Configuration.ShippingOptionProperties");
        if (element != null)
        {
            var properties = new XmlData();
            properties.LoadData(element.ElementProperties);

            properties.SetValue("tabextender", "CMS.Ecommerce");
            properties.SetValue("extenderclassname", "CMS.Ecommerce.Extenders.ShippingOptionTabsExtender");

            element.ElementProperties = properties.GetData();
            element.Update();
        }
    }


    /// <summary>
    /// Removes global shipping options from recycle bin.
    /// </summary>
    private static void RemoveGlobalShippingOptionsFromRecycleBin()
    {
        const string shippingObjectType = "ecommerce.shippingoption";
        var shippingOption = ModuleManager.GetObject(shippingObjectType);

        // Nothing to remove when shipping option type not present
        if (shippingOption == null)
        {
            return;
        }

        // Get histories for all objects of shipping option type
        var histories = ObjectVersionHistoryInfoProvider.GetVersionHistories().WhereEquals("VersionObjectType", shippingObjectType);

        // Fill ids list with IDs of global shipping options present in histories
        var ids = new List<int>();
        foreach (var history in histories)
        {
            if (!String.IsNullOrEmpty(history.VersionXML))
            {
                HierarchyHelper.LoadObjectFromXML(OperationTypeEnum.Export, shippingOption, history.VersionXML);

                if (shippingOption.IsGlobal)
                {
                    ids.Add(shippingOption.Generalized.ObjectID);
                }
            }
        }

        // Remove version histories for selected shipping options
        if (ids.Count != 0)
        {
            var where = new WhereCondition().WhereIn("VersionObjectID", ids);
            ObjectVersionHistoryInfoProvider.DeleteVersionHistories(where.ToString(true));
        }
    }

    #endregion
}